Element源码分析系列7

您所在的位置:网站首页 element ui源码 Element源码分析系列7

Element源码分析系列7

2023-07-22 16:31| 来源: 网络整理| 查看: 265

简介

Element的下拉选择器示意图如下

确实做的很漂亮,交互体验非常好,html有原生的选择器,但是太丑了,而且各浏览器样式不统一,因此要做一个漂亮且实用的下拉选择器必须自己模拟全部方法和结构,Element的下拉选择器代码量非常大,仅select.vue一个文件就快1000行,而且里面是由Element的其他组件组合而成,算上其他组件的话,又得加上1000行,最后是这个选择器引用了非常多的util以及第三方js,再加上这些至少得再加2000行,所以只能分析部分核心原理,下面是下拉选择器的import

import Emitter from 'element-ui/src/mixins/emitter'; import Focus from 'element-ui/src/mixins/focus'; import Locale from 'element-ui/src/mixins/locale'; import ElInput from 'element-ui/packages/input'; import ElSelectMenu from './select-dropdown.vue'; import ElOption from './option.vue'; import ElTag from 'element-ui/packages/tag'; import ElScrollbar from 'element-ui/packages/scrollbar'; import debounce from 'throttle-debounce/debounce'; import Clickoutside from 'element-ui/src/utils/clickoutside'; import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom'; import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'; import { t } from 'element-ui/src/locale'; import scrollIntoView from 'element-ui/src/utils/scroll-into-view'; import { getValueByPath } from 'element-ui/src/utils/util'; import { valueEquals } from 'element-ui/src/utils/util'; import NavigationMixin from './navigation-mixin'; import { isKorean } from 'element-ui/src/utils/shared';

不过这些import里面很多东西是值得学习的,官网代码点此

下拉选择器的html结构

还是先来分析这个下拉选择器的html结构,简化后的html代码如下

node[ctx].documentHandler(e, startClick)); });

这就给document绑定了鼠标按下抬起事件(服务端渲染无效),按下时记录一个按下的dom元素,抬起时遍历所有有该指令的dom,然后执行documentHandler进行判断,该方法如下

function createDocumentHandler(el, binding, vnode) { return function(mouseup = {}, mousedown = {}) { if (!vnode || !vnode.context || !mouseup.target || !mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target)))) return; if (binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName]) { vnode.context[el[ctx].methodName](); } else { el[ctx].bindingFn && el[ctx].bindingFn(); } }; }

注意这个是由createDocumentHandler生成一个documentHandler,里面的第一个if中的el.contains(mouseup.target),el.contains(mousedown.target)就通过原生的contains方法判断点击处是否被el这个dom元素包含,如果是则return,如果不包含,也就是点击在下拉菜单外,则执行vnode.context[el[ctx].methodName]()调用v-clickoutside="handleClose"中的handleClose方法隐藏下拉菜单,el[ctx].methodName是在指令的bind方法里初始化的,如下

bind(el, binding, vnode) { nodeList.push(el); const id = seed++; el[ctx] = { id, documentHandler: createDocumentHandler(el, binding, vnode), methodName: binding.expression, bindingFn: binding.value }; },

将expression赋值给methodName,ctx又是啥?ctx在最上面const ctx = '@@clickoutsideContext'这句话我觉得是给el这个dom加了个属性,这个属性名字2个@开头,表示很特殊,不容易被覆盖,然后这个属性的值是一个对象,里面存储了很多信息,这里的逻辑大体是,在指令第一次被绑定到dom元素时,给dom元素加上要执行的方法等属性,然后给document绑定mouseup事件,后来当用户点击时取出对应的元素的dom进行判断,如果判断为true再取出该dom上之前绑定的方法进行执行

下拉菜单的定位

你可能觉得这个下拉菜单是绝对定位于输入框,那就错了,其实这个下拉框是添加在document.body上的

是不是很神奇,当初始状态没有点击选择框时,这个下拉菜单display:none,这时候是绝对定位且包含在内,见下图

然而当我们点击组件时,这个下拉菜单就跑到body上了

为什么要这样做?官网有说明下拉菜单默认是添加在body上的,不过可以修改。这是因为element用了一个第三方js:popper.js,这个是用来专门处理弹出框的js,1000多行,然后Element又写了个vue-popper.vue来进一步控制,这个文件里有如下代码

createPopper() { ... if (!popper || !reference) return; if (this.visibleArrow) this.appendArrow(popper); if (this.appendToBody) document.body.appendChild(this.popperElm); if (this.popperJS && this.popperJS.destroy) { this.popperJS.destroy(); } ... this.popperJS = new PopperJS(reference, popper, options); this.popperJS.onCreate(_ => { this.$emit('created', this); this.resetTransformOrigin(); this.$nextTick(this.updatePopper); }); },

creatPopper就是初始化时进行的逻辑,里面if (this.appendToBody) document.body.appendChild(this.popperElm)这句话就是关键,通过appendChild将弹出的下拉菜单移动到body上,注意appendChild如果参数是已存在的元素则会移动它。然后你会发现鼠标滚轮滚动时下拉菜单也会随着一起移动,注意下拉菜单是在body上的,那么这里的移动逻辑就是在popperJS里实现的,有点复杂,首先里面得有个addEventListener监听scroll事件,一查果然有

Popper.prototype._setupEventListeners = function() { // NOTE: 1 DOM access here this.state.updateBound = this.update.bind(this); root.addEventListener('resize', this.state.updateBound); // if the boundariesElement is window we don't need to listen for the scroll event if (this._options.boundariesElement !== 'window') { var target = getScrollParent(this._reference); // here it could be both `body` or `documentElement` thanks to Firefox, we then check both if (target === root.document.body || target === root.document.documentElement) { target = root; } target.addEventListener('scroll', this.state.updateBound); this.state.scrollTarget = target; } };

上面的这句话target.addEventListener('scroll', this.state.updateBound);就是绑定了事件监听,继续看updateBound,发现它是通过update方法绑定到this,update如下

/** * Updates the position of the popper, computing the new offsets and applying the new style * @method * @memberof Popper */ Popper.prototype.update = function() { var data = { instance: this, styles: {} }; // store placement inside the data object, modifiers will be able to edit `placement` if needed // and refer to _originalPlacement to know the original value data.placement = this._options.placement; data._originalPlacement = this._options.placement; // compute the popper and reference offsets and put them inside data.offsets data.offsets = this._getOffsets(this._popper, this._reference, data.placement); // get boundaries data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement); data = this.runModifiers(data, this._options.modifiers); if (typeof this.state.updateCallback === 'function') { this.state.updateCallback(data); } };

顾名思义,update就是用来更新弹出框的位置信息,里面是各种子方法进行对应的位置更新



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3